from flask import current_app
from datetime import date
import ldap
import os
import hashlib
import base64

# 模拟用户数据库，仅在 LDAP_ENABLED=False 时使用
# 它将存储批准的用户，以便后续模拟登录
MOCK_USER_DB = {}

def _generate_ssha_hash(password):
    """
    生成一个符合LDAP SSHA格式的密码哈希。
    """
    # 1. 生成一个随机的盐 (salt)
    salt = os.urandom(4)
    
    # 2. 将明文密码和盐拼接
    digest = hashlib.sha1(password.encode('utf-8') + salt)
    
    # 3. 将SHA1哈希值和盐再次拼接
    digest_and_salt = digest.digest() + salt
    
    # 4. 对结果进行Base64编码
    hashed_password = base64.b64encode(digest_and_salt).decode('ascii')
    
    return hashed_password
    # 请将此函数添加到您的 ldap_service.py 文件中，替换旧版本

def get_gid_by_group_name(group_name):
    """
    (已修正) 根据组名查找并返回其 GID (gidNumber)。
    如果组不存在，则返回 None。
    """
    # 关键修正：调用您文件中已存在的 _get_ldap_connection 函数
    conn = _get_ldap_connection()
    if not conn:
        print("获取LDAP连接失败，无法查询GID。")
        return None

    # 从Flask应用上下文中获取配置，与您文件中其他函数保持一致
    base_dn = current_app.config['LDAP_BASE_DN']
    group_ou = current_app.config['LDAP_GROUP_OU']
    search_base = f"{group_ou},{base_dn}"

    try:
        # 构建精确的LDAP搜索过滤器
        search_filter = f"(&(objectClass=posixGroup)(cn={group_name}))"
        
        # 执行搜索，我们只需要gidNumber这一个属性
        results = conn.search_s(
            search_base,
            ldap.SCOPE_SUBTREE,
            search_filter,
            ['gidNumber']
        )

        # 检查是否有搜索结果
        if results:
            # python-ldap 返回结果是一个元组列表 [(dn, attrs), ...]
            # 我们只取第一个结果的属性字典
            dn, entry = results[0]
            if 'gidNumber' in entry:
                # 属性值是字节bytes，需要解码并转为整数
                gid = int(entry['gidNumber'][0].decode('utf-8'))
                return gid
            else:
                # 找到了组但没有gidNumber，这不正常，但做兼容处理
                return None
        else:
            # 如果列表为空，说明没有找到任何条目，该组不存在
            return None

    except ldap.NO_SUCH_OBJECT:
        # 明确处理“未找到对象”的异常
        return None
    except Exception as e:
        print(f"通过组名 '{group_name}' 查询 GID 时出错: {e}")
        return None
    finally:
        # 无论成功与否，都确保断开连接
        if conn:
            conn.unbind_s()

def _get_ldap_connection():
    """私有函数：获取并绑定LDAP连接"""
    try:
        # --- 新增：告诉ldap库不要强制验证TLS证书 ---
        # 这在开发或使用自签名证书时是必需的。
        # 警告：在严格的生产环境中，应配置为验证CA证书。
        ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)

        conn = ldap.initialize(current_app.config['LDAP_SERVER'])
        conn.protocol_version = ldap.VERSION3
        conn.simple_bind_s(
            current_app.config['LDAP_BIND_DN'],
            current_app.config['LDAP_BIND_PASSWORD']
        )
        return conn
    except ldap.LDAPError as e:
        print(f"LDAP 连接失败: {e}")
        raise

def get_next_available_id(id_type='uid'):
    """自动获取下一个可用的 uidNumber 或 gidNumber"""
    if not current_app.config.get('LDAP_ENABLED'):
        # 返回一个基于日期的、看起来合理的模拟ID
        mock_id = int(date.today().strftime("%y%m%d") + "99") 
        print(f"[MOCK_LDAP] Returning mock {id_type} number: {mock_id}")
        return mock_id

    conn = _get_ldap_connection()
    base_dn = current_app.config['LDAP_BASE_DN']
    
    if id_type == 'uid':
        search_filter = "(objectClass=posixAccount)"
        search_dn = f"{current_app.config['LDAP_USER_OU']},{base_dn}"
        attr = 'uidNumber'
        default_start_id = 10000 
    else: # gid
        search_filter = "(objectClass=posixGroup)"
        search_dn = f"{current_app.config['LDAP_GROUP_OU']},{base_dn}"
        attr = 'gidNumber'
        default_start_id = 10000

    try:
        results = conn.search_s(search_dn, ldap.SCOPE_SUBTREE, search_filter, [attr])
        
        if not results:
            return default_start_id

        # --- ↓↓↓ 这是修正后的关键代码行 ↓↓↓ ---
        ids = [int(entry[attr][0]) for dn, entry in results if attr in entry]
        # --- ↑↑↑ 修正结束 ↑↑↑ ---

        return max(ids) + 1 if ids else default_start_id
        
    except ldap.NO_SUCH_OBJECT:
        return default_start_id
    finally:
        conn.unbind_s()

import subprocess

def reset_password(username, new_password):
    """
    使用 ldappasswd 命令重置指定用户的密码。
    这比直接修改LDAP条目更安全。
    """
    # 从Flask配置中获取LDAP的Bind DN和密码，用于认证
    # 注意：这里需要你根据实际配置调整
    # 例如：app.config['LDAP_BIND_USER_DN'] 和 app.config['LDAP_BIND_USER_PASSWORD']
    # 如果你的 ldap.conf 配置了 rootbinddn，可能不需要 -D 和 -w 参数
    
    # 构建目标用户的 DN (Distinguished Name)
    # 假设你的用户DN结构是 "uid=<username>,ou=people,dc=example,dc=com"
    # 请根据你的LDAP结构修改
    user_dn = f"uid={username},ou=people,dc=saidshpc,dc=com" # <--- !! 请根据你的实际情况修改这个DN !!

    try:
        # 使用 subprocess 调用 ldappasswd 命令
        # -s 后面是新密码
        # -x 使用简单认证
        # -D 和 -w 是管理员绑定凭据，如果需要的话
        # 如果你的服务器配置允许管理员免密操作，可以去掉 -D 和 -w
        command = [
            'ldappasswd',
            '-s', new_password,
            '-x',
            # '-D', 'cn=admin,dc=saidshpc,dc=com', # <--- 管理员Bind DN，如果需要
            # '-w', 'YOUR_ADMIN_PASSWORD',        # <--- 管理员密码，如果需要
            user_dn
        ]
        
        print(f"正在为用户DN '{user_dn}' 重置密码...")
        # 执行命令
        result = subprocess.run(command, check=True, capture_output=True, text=True)
        print("LDAP密码重置成功。")
        return True, None

    except subprocess.CalledProcessError as e:
        error_message = f"重置LDAP密码失败: {e.stderr}"
        print(error_message)
        return False, error_message
    except Exception as e:
        error_message = f"执行ldappasswd时发生未知错误: {e}"
        print(error_message)
        return False, error_message

def format_group_name(supervisor_name):
    """
    将导师名格式化为符合LDAP标准的组名
    例如：'张三教授' -> 'group_zhangsan'
    """
    import re
    
    # 移除特殊字符，转换为小写
    clean_name = re.sub(r'[^\w\u4e00-\u9fff]', '', supervisor_name)
    
    # 如果包含中文，可以考虑使用拼音库转换，这里简化处理
    # 你可以安装 pypinyin 库来实现中文转拼音
    group_name = f"group_{clean_name.lower()}"
    
    # 确保组名长度合理
    if len(group_name) > 32:
        group_name = group_name[:32]
    
    return group_name

def get_user_groups(username):
    """获取用户所属的所有组"""
    if not current_app.config.get('LDAP_ENABLED'):
        print(f"[MOCK_LDAP] Getting groups for user '{username}' from mock database.")
        # 在模拟模式下，可以返回一个假的组列表
        user = MOCK_USER_DB.get(username)
        if user:
            return [user.get('group', 'group_default')]
        return []

    conn = _get_ldap_connection()
    base_dn = current_app.config['LDAP_BASE_DN']
    group_ou = current_app.config['LDAP_GROUP_OU']
    
    search_filter = f"(memberUid={username})"
    
    try:
        results = conn.search_s(f"{group_ou},{base_dn}", ldap.SCOPE_SUBTREE, search_filter, ['cn'])
        groups = [entry['cn'][0].decode('utf-8') for dn, entry in results if 'cn' in entry]
        return groups
    except Exception as e:
        print(f"获取用户组时出错: {e}")
        return []
    finally:
        conn.unbind_s()


def create_user(username, password, full_name, email, totp_secret, uid_number, gid_number, home_directory):
    """(已修正) 在LDAP中创建新用户，完善了对单字姓名的处理"""
    conn = _get_ldap_connection()
    base_dn = current_app.config['LDAP_BASE_DN']
    user_ou = current_app.config['LDAP_USER_OU']
    user_dn = f"uid={username},{user_ou},{base_dn}"
    
    shadow_last_change = (date.today() - date(1970, 1, 1)).days

    # --- 核心修改：完善姓名处理逻辑 ---
    if len(full_name) > 1:
        # 如果姓名多于一个字，按常规拆分
        sn_str = full_name[0]
        given_name_str = full_name[1:]
    else:
        # 如果姓名只有一个字，则姓和名都设为该字，避免空字符串
        sn_str = full_name
        given_name_str = full_name
    # --- 修改结束 ---

    user_attrs = [
        ('objectClass', [b'inetOrgPerson', b'posixAccount', b'shadowAccount']),
        ('uid', [username.encode('utf-8')]),
        ('cn', [full_name.encode('utf-8')]),
        ('displayName', [full_name.encode('utf-8')]),
        ('sn', [sn_str.encode('utf-8')]), # 使用处理后的姓
        ('givenName', [given_name_str.encode('utf-8')]), # 使用处理后的名
        ('mail', [email.encode('utf-8')]),
        ('userPassword', [f"{{SSHA}}{_generate_ssha_hash(password)}".encode('utf-8')]), # 确保调用内部哈希函数
        ('description', [f"TOTP Secret: {totp_secret}".encode('utf-8')]),
        ('uidNumber', [str(uid_number).encode('utf-8')]),
        ('gidNumber', [str(gid_number).encode('utf-8')]),
        ('homeDirectory', [home_directory.encode('utf-8')]),
        ('loginShell', [b'/bin/bash']),
        ('shadowLastChange', [str(shadow_last_change).encode('utf-8')])
    ]
    
    try:
        conn.add_s(user_dn, user_attrs)
        print(f"LDAP 用户 '{username}' 创建成功。")
    except ldap.ALREADY_EXISTS:
        print(f"LDAP 用户 '{username}' 已存在。")
        raise
    finally:
        conn.unbind_s()
def create_group_if_not_exists(group_name, gid_number):
    """如果组不存在，则创建新组，或在模拟模式下跳过"""
    if not current_app.config.get('LDAP_ENABLED'):
        print(f"[MOCK_LDAP] Simulating group creation for '{group_name}'.")
        return # 直接返回

    conn = _get_ldap_connection()
    base_dn = current_app.config['LDAP_BASE_DN']
    group_ou = current_app.config['LDAP_GROUP_OU']
    group_dn = f"cn={group_name},{group_ou},{base_dn}"

    try:
        conn.search_s(group_dn, ldap.SCOPE_BASE)
        print(f"LDAP 组 '{group_name}' 已存在。")
    except ldap.NO_SUCH_OBJECT:
        print(f"LDAP 组 '{group_name}' 不存在，正在创建...")
        group_attrs = [
            ('objectClass', [b'posixGroup', b'top']),
            ('cn', [group_name.encode('utf-8')]),
            ('gidNumber', [str(gid_number).encode('utf-8')])
        ]
        conn.add_s(group_dn, group_attrs)
        print(f"LDAP 组 '{group_name}' 创建成功。")
    finally:
        conn.unbind_s()

def add_user_to_group(username, group_name):
    """将用户添加到组，或在模拟模式下跳过"""
    if not current_app.config.get('LDAP_ENABLED'):
        print(f"[MOCK_LDAP] Simulating adding user '{username}' to group '{group_name}'.")
        # 在模拟模式下，更新用户的组信息
        if username in MOCK_USER_DB:
            MOCK_USER_DB[username]['group'] = group_name
        return # 直接返回
    
    conn = _get_ldap_connection()
    base_dn = current_app.config['LDAP_BASE_DN']
    group_ou = current_app.config['LDAP_GROUP_OU']
    group_dn = f"cn={group_name},{group_ou},{base_dn}"

    # 定义要执行的修改操作：在 memberUid 属性中添加一个值
    mod_attrs = [(ldap.MOD_ADD, 'memberUid', [username.encode('utf-8')])]

    try:
        conn.modify_s(group_dn, mod_attrs)
        print(f"已将用户 '{username}' 添加到组 '{group_name}'。")
    except ldap.TYPE_OR_VALUE_EXISTS:
        print(f"用户 '{username}' 已在组 '{group_name}' 中。")
    except Exception as e:
        print(f"添加用户到组时出错: {e}")
        raise
    finally:
        conn.unbind_s()


def authenticate_user(username, password):
    """验证用户凭据，或在模拟模式下验证"""
    if not current_app.config.get('LDAP_ENABLED'):
        print(f"[MOCK_LDAP] Authenticating '{username}' against mock database.")
        user = MOCK_USER_DB.get(username)
        # 检查用户是否存在，并且密码是否匹配
        if user and user['password'] == password:
            print(f"[MOCK_LDAP] Mock authentication successful for '{username}'.")
            return True
        print(f"[MOCK_LDAP] Mock authentication failed for '{username}'.")
        return False

    conn = _get_ldap_connection()
    base_dn = current_app.config['LDAP_BASE_DN']
    user_ou = current_app.config['LDAP_USER_OU']
    user_dn = f"uid={username},{user_ou},{base_dn}"

    # 初始化一个新的LDAP连接，用于尝试用户绑定
    try:
        conn = ldap.initialize(current_app.config['LDAP_SERVER'])
        conn.protocol_version = ldap.VERSION3
        
        # 最关键的一步：直接用用户的 DN 和密码尝试绑定
        # 如果成功，不抛出任何异常。
        # 如果失败，会抛出 ldap.INVALID_CREDENTIALS 异常。
        conn.simple_bind_s(user_dn, password)
        
        print(f"用户 '{username}' 验证成功。")
        conn.unbind_s()
        return True
        
    except ldap.INVALID_CREDENTIALS:
        print(f"用户 '{username}' 密码错误。")
        return False
    except ldap.NO_SUCH_OBJECT:
        print(f"用户 '{username}' 不存在。")
        return False
    except ldap.LDAPError as e:
        print(f"LDAP 验证时出现未知错误: {e}")
        return False
